"""

.. module:: httpserver
.. moduleauthor:: @author: 

This module supplies HTTP thread, server and request handlers used by the HTTP Task. 
   
.. seealso:: modules :mod:`<module>`

*Constants*:
.. data:: constant_1
        
    constant_1 is used for ......
        
.. data:: constant_2
        
    constant_2 is used for ......
"""

# Imports
#
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from Queue import Queue
from SocketServer import ThreadingMixIn
from threading import Thread
import cgi
import httplib
import logging

from datastore.resource import *
from datastore.uri import *
from runtime.spaces import *
from services.htmltemplate import HTMLTemplate



# Constants
#
GET = 'GET'
PUT = 'PUT'
DELETE = 'DELETE'
POST = 'POST'

# Classes
#

class HTTPThread(Thread):
    """This class is started with a new site is registered. This class is a 
    daemon thread, which runs the MultiThreadedHTTPServer. This free's the 
    task up to process additional messages.
    
    This class and its data structures are not available outside the HTTP task.

    *Usage examples*::

        example code

    *Common Arguments for Class methods*:
        <arguments>
    
    *Audience*:
        <audience>
    
    *Constants*:
        .. data:: constant_1
        
            constant_1 is used for ......
        
        .. data:: constant_2
        
            constant_2 is used for ......
    """
    
    def __init__(self, spaces, ip_address, port):
        # Constructor
        #
        # Parameters:
        #     ip_address :
        #     port :
        #
        
        Thread.__init__(self)
        
        self.logger = logging.getLogger('yarra.services.httpserver.HTTPThread')
        self.logger.log(logging.DEBUG, "spaces (%s), ip_address (%s), port (%s)" % (spaces, ip_address, port))
        
        self.spaces = spaces
        self.ip_address = ip_address
        self.port = port
        
        # dictionary, indexed by site title, of dictionaries containing:
        # - hostname
        # - action list
        # - site template uri
        # - result template uris
        #
        self.sites_dict = {}
        
        # the default site name. The default site is used when a request comes in 
        # that contains no 'Host:' or an unknown 'Host:'
        #
        self.default_site = ""
        
    def run(self):
        """This function is called from Thread.start() once the new Thread is created.
        This is the mainline of the thread.
        """
        
        self.logger.log(logging.DEBUG, "HTTPThread started")
        
#        url = self.site.listen.url()
        self.server = MultiThreadedHTTPServer((self.ip_address, self.port), RequestHandler)
        
        # The RequestHandler requires knowledge of spaces so it can pass HTTP 
        # requests to other processes. We pass our knowledge of spaces to RequestHandler 
        # by inserting spaces into the server (MultiThreadedHTTPServer) instance.
        # The RequestHandler (automatically) sees the server instance via 
        # 'RequestHandler.server',  made available by BaseHTTPRequestHandler.
        #
        # We'll also pass the 'sites_dict' dictionary so the RequestHandler can
        # pass requests to the correct site task based on incoming 'Host:'.
        #
        # We'll also pass the 'default_site' for those requests that contain no
        # 'Host:' header.
        #
        self.server.spaces = self.spaces
        self.server.sites_dict = self.sites_dict
        self.server.default_site = self.default_site
        
        self.server.serve_forever()
        
        self.logger.log(logging.DEBUG, "HTTPThread completed")
        
    def shutdown(self):
        """This function overrides BaseServer::shutdown().
        """
        
        pass
    
    def add_site(self, title, hostname="", default=False, templateuri="", action_list=[], transforms_dict={}):
        """This function adds a new site to the server thread. 
        """
        
        self.logger.log(logging.DEBUG, "title (%s), hostname (%s), default (%s), templateuri (%s), action_list (%s), transforms_dict (%s)" % (title, hostname, default, templateuri, action_list, transforms_dict))
        
        # Don't add the site title/hostname if we already have the title.
        if (self.sites_dict.has_key(title) is not True):
            dict = {}
            dict['hostname'] = hostname
            dict['actions'] = action_list
            dict['templateuri'] = templateuri
            dict['transforms'] = transforms_dict
            self.sites_dict[title] = dict
            
            if (default is True) and (len(self.default_site) == 0):
                self.default_site = title
            
            self.logger.log(logging.DEBUG, "site (%s) added." % title)
            self.logger.log(logging.DEBUG, "sites_dict (%s), default_site (%s)" % (self.sites_dict, self.default_site))
    
    def delete_site(self, title):
        """This function deletes a site from the server thread. 
        """
        
        self.logger.log(logging.DEBUG, "title (%s)" % title)
        
        if (self.sites_dict.has_key(title) is True):
            # Note: We keep the HTTP server running even if there are no sites
            #       registered. The server can only be stopped with shutdown().
            #
            del self.sites_dict[title]
            if (self.default_site == title):
                self.default_site = ""
            
            self.logger.log(logging.DEBUG, "site (%s) deleted." % title)
            self.logger.log(logging.DEBUG, "sites_dict (%s), default_site (%s)" % (self.sites_dict, self.default_site))
    
    def set_hostname(self, title, hostname):
        """This function sets the hostname for a site title previously registered
        with 'add_site()'. 
        """
        
        self.logger.log(logging.DEBUG, "title (%s), hostname (%s)" % (title, hostname))
        
        if (self.sites_dict.has_key(title) is True):
            self.sites_dict[title] = hostname
            self.logger.log(logging.DEBUG, "sites_dict (%s)" % self.sites_dict)
            
    
class MultiThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """This class processes incoming messages using the standard Python HTTP
    server superclass. Each request handler is given its own thread, which 
    exists until processing of the request is complete.
    
    This class and its data structures are not available outside the HTTP task.

    *Usage examples*::

        example code

    *Common Arguments for Class methods*:
        <arguments>
    
    *Audience*:
        <audience>
    
    *Constants*:
        .. data:: constant_1
        
            constant_1 is used for ......
        
        .. data:: constant_2
        
            constant_2 is used for ......
    """
    
    # Class constants
    #
    allow_reuse_address = True
    
    def __init__(self, server_address, RequestHandlerClass):
        # Constructor
        #
        # Parameters:
        #     server_address :
        #     RequestHandlerClass : 
        #
        
        HTTPServer.__init__(self, server_address, RequestHandlerClass)
        
        self.logger = logging.getLogger('yarra.services.httpserver.MultiThreadedHTTPServer')
        self.logger.log(logging.DEBUG, "********** MultiThreadedHTTPServer::__init__()")
    
    def serve_forever(self):
        """This function overrides BaseServer::server_forever(). This runs until 
        shutdown() is called. Through handle_request() a new thread is created 
        for the RequestHandlerClass to handle each request.
        """
        
        self.logger.log(logging.DEBUG, "********** MultiThreadedHTTPServer Started")
        self.stop = False
        while (not self.stop):
            self.handle_request()
        
        self.logger.log(logging.DEBUG, "MultiThreadedHTTPServer Stopped")
        
    def shutdown(self):
        """This function overrides BaseServer::shutdown().
        """
        
        self.logger.log(logging.DEBUG, "MultiThreadedHTTPServer Shutdown")
        
        self.stop = True

class RequestHandler(BaseHTTPRequestHandler):
    """This class is the request handler for all HTTP requests.
    
    This class and its data structures are not made available outside the HTTP task.

    *Usage examples*::

        example code

    *Common Arguments for Class methods*:
        <arguments>
    
    *Audience*:
        <audience>
    
    *Constants*:
        .. data:: constant_1
        
            constant_1 is used for ......
        
        .. data:: constant_2
        
            constant_2 is used for ......
    """
    
    # Class constants
    #
    
    def __init__(self, request, client_address, server):
        # Constructor.
        #
        # Parameters:
        #
        
        # Note: The call to 'BaseHTTPRequestHandler.__init__()' results in a
        #       call to 'handle_request()'. So we must initialise RequestHandler
        #       before initialising the parent.
        #
        # Note: In HTTPThread we set 'server.spaces' to HTTPThread's spaces reference.
        #
        
        self.queue = Queue()
        self.spaces_context = SpacesContext(server.spaces, self.queue)
        
        self.logger = logging.getLogger('yarra.services.httpserver.RequestHandler')
        self.logger.log(logging.DEBUG, "request (%s), client_address (%s), server (%s)" % (request, client_address, server))
        
        BaseHTTPRequestHandler.__init__(self, request, client_address, server)
    
    def handle_request(self, method, data=None):
        """This function handles HTTP requests.
        
        :param method: String - HTTP method invoked.
        """
        
        self.logger.log(logging.DEBUG, "********** command (%s), path (%s), request_version (%s), method (%s), data (%s)" % (self.command, self.path, self.request_version, method, data))
        self.logger.log(logging.DEBUG, "********** headers (%s)" % self.headers)
        
        rs = Resource()
        hostname = None
        dict = None
        site_title = ""
        
        # 1. get all (interesting) HTTP headers and insert into Resource.
        #    - "Host:"
        #    - "Cookie:"
        #    - "Content-Length:"
        #    - "Content-Type:" (from POSTs)
        #    - "Referer:"
        #    - Method (GET, POST, etc)
        #    - Request URI
        #    - Request data (from POSTs)
        #
        header_host = self.headers.get("Host")
        if (header_host is not None) and (len(header_host) > 0):
            (hostname, port) = header_host.split(":")
        cookie = self.headers.get("Cookie")
        content_length = self.headers.get("Content-Length")
        content_type = self.headers.get("Content-Type")
        referer = self.headers.get("Referer")
        
        if (hostname is not None) and (len(hostname) > 0):
            rs.host = hostname
        if (cookie is not None) and (len(cookie) > 0):
            rs.cookie = cookie
        if (content_length is not None) and (len(content_length) > 0):
            rs.contentlength = content_length
        if (content_type is not None) and (len(content_type) > 0):
            rs.contenttype = content_type
        if (referer is not None) and (len(referer) > 0):
            rs.referer = referer
        rs.method = method
        rs.uri = Uri(self.path)
        if (data is not None) and (len(data) > 0):
            rs.data, data
        
        self.logger.log(logging.DEBUG, "********** request resource (%s)" % rs.pretty_print())
        
        http_status = httplib.OK
        
        # Inspect the "Host:" header to see if the hostname has been registered 
        # for the port we are listening on.  
        # - if so, pass the request to the associated action tasks.
        # - if not, pass the request to the action tasks of the default site.

        if (hostname is not None):
            for title in self.server.sites_dict.keys():
                d = self.server.sites_dict[title]
                if (hostname == d['hostname']):
                    dict = d
                    site_title = title
        
        if (dict is None):
            # find the default site, if one
            #
            dict = self.server.sites_dict[self.server.default_site]
            site_title = self.server.default_site
        
        if (dict is not None):
            transforms_dict = dict['transforms'] 
            if (transforms_dict is not None):
                self.logger.log(logging.DEBUG, "********** using site (%s)" % site_title)
                
                result = None
                error = None
                
                # 2. process action list.
                #    - each action takes the Resource, processes it, and returns it.
                #    - each action is either a request to a task or a fragment of code to execute.
                #    - an action can return an error in the Resource. Action list processing
                #      will terminate if an error is detected.
                #    - an action returns a result string to determine the final transformation.
                #
                action_list = dict['actions']
                self.logger.log(logging.DEBUG, "********** action_list (%s)" % (action_list))
                
                for action_dict in action_list:
                    title = action_dict['dc:title']
                    self.logger.log(logging.DEBUG, "********** title (%s)" % (title))
                    
                    try:
                        taskuri = action_dict['package:taskuri']
                        self.logger.log(logging.DEBUG, "********** taskuri (%s)" % (taskuri))
                    except KeyError:
                        taskuri = None
                    
                    try:
                        code_fragment = action_dict['services:type/http/site/action/code']
                        self.logger.log(logging.DEBUG, "********** code_fragment (%s)" % (code_fragment))
                    except KeyError:
                        code_fragment = None
                    
                    if (taskuri is not None):
                        # Action = Task.
                        #
                        parameters_dict = action_dict['services:type/http/site/action/parameters']
                        
                        self.logger.log(logging.DEBUG, "********** using action task (%s)(%s)" % (title, taskuri))
                        
                        # set parameters on Resource.
                        #
                        rs.set_var('services:type/http/site/action/parameters', parameters_dict)
                        
                        # Send Resource to server task and wait for response.
                        #
                        # Note: The convention for a server task to process or watch for Spaces requests
                        #       restricts the server from listening to:
                        #
                        #       <task_uri>.collection(True)
                        #
                        #       This is because the convention for server tasks states that responses to clients
                        #       will have Uris of the format:
                        #
                        #       <task_uri>.add("requests").add(<uuid>).add("response")
                        #
                        #       Therefore a server task can listen to anything under specific Uri
                        #       parts. For example:
                        #
                        #       <task_uri>.add("listen")
                        #       <task_uri>.add("XYZ").collection(True)
                        #       
                        #       The convention is to have the server task to process on "puts" to:
                        #       <task_uri>.add("listen")
                        #       to process http requests "delegated" to it.
                        #                        
                        new_uri = self.spaces_context.put(taskuri.add("listen"), rs)
                        rs = self.spaces_context.get_wait(new_uri.response)

                        self.logger.log(logging.DEBUG, "********** response from put() (%s)" % rs)

                    elif (code_fragment is not None):
                        # Action is python code fragment.
                        #
                        # Note: We must strip() leading and trailing spaces otherwise 
                        #       we get an indentation error.
                        # 
                        self.logger.log(logging.DEBUG, "********** executing code fragment (%s)" % code_fragment.strip())
                        exec code_fragment.strip()
                        
                    # check if we received an error
                    #
                    try:
                        error = rs.get_var('services:type/http/site/action/error')
                        self.logger.log(logging.ERROR, "Error received from action (%s)(%s) for site (%s)" % (title, taskuri, site_title))
                        break
                    except KeyError:
                        pass
                    
                    # get the result string from this action. If this is the final 
                    # action then this string is used to find the output template.
                    # 
                    # Note: We're only interested in the result from the last action.
                    #
                    try:
                        result = None
                        result = rs.get_var('services:type/http/site/action/result')
                    except KeyError:
                        pass
                
                # 3. transform Resource to output format. The MIME-type of the output is
                #    defined by the template.
                #
                templateuri = None
                if (error is not None):
                    # use error template, if one.
                    #
                    templateuri = transforms_dict["error"]
                    if (templateuri is None):
                        self.logger.log(logging.ERROR, "Cannot find 'error' template for site (%s) definition." % site_title)
                    else:
                        self.logger.log(logging.ERROR, "********** using 'error' transformation, uri (%s)" % templateuri)
                    
                elif (result is not None):
                    templateuri = transforms_dict[result]
                    if (templateuri is None):
                        self.logger.log(logging.ERROR, "Cannot find '(%s)' template for site (%s) definition." % (result, site_title))
                    else:
                        self.logger.log(logging.ERROR, "********** using result (%s) transformation, uri (%s)" % (result, templateuri))
                    
                else:
                    # use default template
                    #
                    templateuri = transforms_dict["default"]
                    if (templateuri is None):
                        self.logger.log(logging.ERROR, "Cannot find 'default' template for site (%s) definition." % site_title)
                    else:
                        self.logger.log(logging.ERROR, "********** using 'default' transformation, uri (%s)" % templateuri)
                    
                if (templateuri is not None):
                    # Find the template Resource.
                    #
                    template_rs = self.spaces_context.get(templateuri)
                    if (template_rs is None):
                        self.logger.log(logging.ERROR, "Cannot find template Resource (%s) in Spaces for site (%s)" % (templateuri, site_title))
                    else:
                        self.logger.log(logging.DEBUG, "********** using template resource (%s)" % template_rs)
                        
                        try:
                            content_type = "text/html"
                        except KeyError:
                            content_type = ""
                        
#                        # Get the site template.
#                        #
#                         rs.site_template = dict[str(Resource.KEY_URI_SERVICES_TYPE_HTTP_SITE_TEMPLATEURI)]
#                        self.logger.log(logging.DEBUG, "********** using site transformation uri (%s)" % rs.site_template)
                        
#                        self.logger.log(logging.ERROR, "********** namespaces_dict (%s)" % rs.get_var('namespaces_dict'))
#                        self.logger.log(logging.ERROR, "********** namespaces_dict (%s)" % rs.get_var('abcdef'))
                        
                        # Perform the transformation.
                        #
#                        site_template_rs = self.spaces_context.get(rs.site_template)
                        site_template_rs = self.spaces_context.get(dict['templateuri'])
                        htmltemplate_uri = self.spaces_context.uri('htmltemplate:')
#                        if site_template_rs.metadata_has_key(htmltemplate_uri.mako) and site_template_rs.get_single_metadata(htmltemplate_uri.mako):
#
#                            mako_http_template = MakoHTTPTemplate(self.spaces_context, site_template_rs, template_rs, rs)
#                            html_data = mako_http_template.render()
#                        else:
                        htmlTemplate = HTMLTemplate(self.spaces_context, site_template_rs, template_rs, rs, verbose=False)
                        html_data = htmlTemplate.generate()
                        self.logger.log(logging.DEBUG, "********** html_data (%s)" % html_data)
                        
                        if not rs.has_var('http_response_code'):
                            # default response code Return 200 - OK
                            rs.http_response_code = httplib.OK
                        if not rs.has_var('http_headers'):
                            # no additional headers
                            rs.http_headers = {}
                        # Send the response to the user.
                        self.send_http_response(rs.http_response_code, content_type=content_type, content_length=len(html_data), data=html_data, headers=rs.http_headers)
                else:
                    # No transformation template defined. Return 500 - Internal Server Error
                    #
                    http_status = httplib.INTERNAL_SERVER_ERROR
            else:
                # No transformation templates defined. Return 500 - Internal Server Error
                #
                http_status = httplib.INTERNAL_SERVER_ERROR
        else:
            # No site configured to handle this request. Return 404 - File Not Found.
            #
            http_status = httplib.NOT_FOUND
        
        if (http_status != httplib.OK):
            self.send_http_response(http_status, content_type=None, content_length=0, data=None)
    
    def do_GET(self):
        """This function overrides BaseHTTPRequestHandler::do_GET().
        """
        
        self.handle_request(GET)
        return
    
    def do_POST(self):
        """This function overrides BaseHTTPRequestHandler::do_POST().
        """
        
        # FIXME: should be pass 'post' or 'put'????
        
        # Parse the form data posted
        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={'REQUEST_METHOD':'POST',
                     'CONTENT_TYPE':self.headers['Content-Type'],
                     })
        data = {}
        for key in form.keys():
            data[key] = form[key].value
        self.handle_request(POST, data)
        return
    
    def do_PUT(self):
        """This function overrides BaseHTTPRequestHandler::do_PUT().
        """
        
        input_stream = self.rfile
        content_length = int(self.headers['content-length'])
        body = input_stream.read(content_length)
        self.handle_request(PUT, body)
        return
    
    def do_DELETE(self):
        """This function overrides BaseHTTPRequestHandler::do_DELETE().
        """
        
        self.handle_request(DELETE)
        return
    
    def send_http_response(self, http_status, content_type=None, content_length=0, data=None, headers=None):
        """This function creates the HTTP message and sends it to the client.
        
        :param http_status: int - HTTP status code
        :param content_type: String - HTTP "Content-Type:".
        :param content_length: int - HTTP "Content-Length:".
        :param data: String - output.
        """
        
        self.logger.log(logging.DEBUG, "********** sending HTTP response, status (%s), content_type (%s), content_length (%s), data (%s)" % (http_status, content_type, content_length, data))
        
        self.send_response(http_status)
        self.send_header('Cache-control', 'no-cache')
        if (content_type is not None):
            self.send_header('Content-Type', content_type)
        self.send_header('Content-Length', content_length)
        for header_key, header_value in headers.items():
            self.send_header(header_key, header_value)
        self.end_headers()
        
        if (data is not None):
            self.wfile.write(data)
        
        self.wfile.close()
            
    def request_spaces_context(self, context, url, method, data):
        """This function calls spaces depending on the http method. Returns the 
        resource of the response from Spaces.
        
        :param context: runtime.spaces.SpacesContext
        :param url: String - url that has been requested by the caller.
        :param method: String - http method associated with the request.
        :param data: String - serialised resource as a string. This is the data 
        passed along with the request.
        :rtype: datastore.resource.Resource
        """
        
        uri = Uri(url)
        self.logger.log(logging.DEBUG, "%s request to %s for resource %s" % (method, url, uri.full()))
        res = Resource(data)
        if method == GET:
            return context.get(uri)
        elif method == PUT:
            new_uri = context.put(uri, res)
            res = Resource()
            res.content = new_uri
            return res
        elif method == DELETE:
            return context.delete(uri)
        else:
            # spaces does not handle any other http methods
            raise Exception('Cannot handle method %s' % method)